// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//--------------------------------------------------------------------------------------
// TelemetryManager.h
//
// Publishing telemetry functionality
//
// Advanced Technology Group (ATG)
// Copyright (C) Microsoft Corporation. All rights reserved.
//--------------------------------------------------------------------------------------

#include "TelemetryManager.h"
#include "TelemetryPCH.h"
#include "TelemetryService.h"
#include "Telemetry.h"
#include "Interfaces/IPluginManager.h"
#include "TelemetryJson.h"
#include "SimplygonSettings.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "SimplygonModule.h"

FString FTelemetryService::AuthenticationKey;
bool FTelemetryService::IsInitialized = false;
bool FTelemetryService::UseKey = false;


TUniquePtr<FTelemetryManager> FTelemetryManager::Instance;
bool hasInit = false;

TAtomic<uint32> Sequence;

#define ENABLED_TELEMETRY 1
#define DEBUG_TELEMETRY 0

class FTelemetryBatchPayload
{
public:
    FTelemetryBatchPayload(const FTelemetryProperties &Common) :
        Payload(),
        Writer(TJsonWriterFactory<>::Create(&Payload)),
		IsFinalized(false)
    {
        Writer->WriteObjectStart(); // Outer most object
        Writer->WriteObjectStart("header"); // header portion
        FTelemetryJsonSerializer::Serialize(Common, Writer);
        Writer->WriteObjectEnd();
        Writer->WriteArrayStart("events"); // Events array
    }

    void AddTelemetry(FTelemetryProperties Event)
    {
        check(!IsFinalized);

        Writer->WriteObjectStart();
        FTelemetryJsonSerializer::Serialize(Event, Writer);
        Writer->WriteObjectEnd();
    }

    const FString & Finalize()
    {
        check(!IsFinalized); // Shouldn't call finalize more than once

        if (!IsFinalized) // Nothing really bad happens though...
        {
            Writer->WriteArrayEnd();
            Writer->WriteObjectEnd();
            Writer->Close();
            IsFinalized = true;
        }

        check(Payload.IsEmpty() == false);
        return Payload;
    }

    const FString &GetPayload() { return Payload; }

private:
    FString Payload;
    TSharedRef<TJsonWriter<>> Writer;
    bool IsFinalized;
};

class FTelemetryWorker : public FRunnable
{

public:
	FTelemetryWorker(double SendInterval, int PendingBufferSize = 127, bool Compress = false):
		SendInterval(SendInterval),
		ShouldRun(true),
		IsComplete(false),
		UseCompression(Compress),
		Pending(PendingBufferSize)
	{
		Thread.Reset(FRunnableThread::Create(this, TEXT("SimplygonTelemetryUploadThread"), 0, EThreadPriority::TPri_Lowest));

	}

	virtual ~FTelemetryWorker()
	{	
		if( Sync )
		{
			FGenericPlatformProcess::ReturnSynchEventToPool( Sync );
			Sync = nullptr;
		}
	}

	virtual bool Init() override
	{
		
		Sync = FGenericPlatformProcess::GetSynchEventFromPool();
		check(Sync);
		return true;
	}

	virtual uint32 Run() override
	{
		while (ShouldRun)
		{
			Sync->Wait(FTimespan::FromSeconds(SendInterval));

			FScopeLock Lock(&CriticalSection);
			if( ShouldRun && Pending.Count() > 0 )
			{
				SendTelemetry(FTelemetryManager::Get().GetCommonProperties());
			}
		}

		return 0;
	}

	virtual void Stop() override
	{
		ShouldRun = false;

		TriggerFlush();

		IsComplete = true;
	}

	virtual void Exit() override
	{
		if (!IsComplete)
		{
			Stop();
			Thread->WaitForCompletion();

			if (Sync)
			{
				FGenericPlatformProcess::ReturnSynchEventToPool(Sync);
				Sync = nullptr;
			}
		}
	}

	void TriggerFlush()
	{
		if (Sync)
		{
			Sync->Trigger();
		}
	}

	void Enqueue(TSharedPtr<FTelemetryBuilder> Properties)
	{
		// Circular Queue/Buffer want SPSC model, so limit to the most likely thread telemetry will be generated on
		if (!IsInGameThread())
		{
			AsyncTask(ENamedThreads::GameThread, [this, Properties]() {
				FScopeLock Lock(&CriticalSection);
				Pending.Enqueue(Properties);
				});
		}
		else
		{
			FScopeLock Lock(&CriticalSection);
			Pending.Enqueue(Properties);
		}
	}

	bool CompressPayload(const FString& Payload, TArray<uint8>& Compressed)
	{
		FTCHARToUTF8 PayloadUTF8(*Payload);

		const void* UncompressedPtr = PayloadUTF8.Get();
		const int32 UncompressedSize = PayloadUTF8.Length() + 1;
		int32 CompressedSize = 0;

		Compressed.AddUninitialized(UncompressedSize * 4 / 3);
		bool CompressSuccessful = FCompression::CompressMemory(TEXT("text"), Compressed.GetData(), CompressedSize, UncompressedPtr, UncompressedSize, (ECompressionFlags)(COMPRESS_GZIP | COMPRESS_BiasMemory));
		if (CompressSuccessful)
		{
			Compressed.SetNum(CompressedSize, false);
		}

		return CompressSuccessful;
	}

	virtual void SendTelemetry(const FTelemetryProperties& CommonProperties) = 0;

protected:

	double SendInterval;
	FThreadSafeBool ShouldRun;
	FThreadSafeBool IsComplete;
	FThreadSafeBool UseCompression;

	mutable FCriticalSection CriticalSection;

	//NOTE: This is a lock free container only under one producer one consumer. 
	TCircularQueue<TSharedPtr<FTelemetryBuilder>> Pending;

private:
	TUniquePtr<FRunnableThread> Thread;
	FEvent* Sync;

};


/**
 * @brief A telemetry worker for custom http endpoint
*/
class FHttpTelemetryWorker : public FTelemetryWorker
{
public:

	FHttpTelemetryWorker(FString IngestUrl, double SendInterval, int PendingBufferSize = 127, bool Compress = false) :
		FTelemetryWorker(SendInterval,PendingBufferSize,Compress),
		IngestUrl(IngestUrl)
    {
    }

	virtual ~FHttpTelemetryWorker() {}

    virtual void SendTelemetry(const FTelemetryProperties &CommonProperties)
    {
#ifdef ENABLED_TELEMETRY
        auto request = FTelemetryService::CreateServiceRequest();

        FTelemetryBatchPayload BatchPayload(CommonProperties);

        TSharedPtr<FTelemetryBuilder> Event;
        while (Pending.Dequeue(Event))
        {
            BatchPayload.AddTelemetry(Event->GetProperties());
        }

        request->SetURL(IngestUrl);
        request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
        request->SetHeader(TEXT("x-ms-payload-type"), TEXT("batch"));
        request->SetVerb(TEXT("POST"));

        const FString &Payload = BatchPayload.Finalize();
        TArray<uint8> Compressed;

        if (CompressPayload(Payload, Compressed))
        {
            request->SetContent(Compressed);
            request->SetHeader(TEXT("Content-Encoding"), TEXT("gzip"));
        }
        else
        {
            request->SetContentAsString(Payload);
        }

        request->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr req, FHttpResponsePtr resp, bool successful)
        {
            UE_LOG(LogTelemetry, Display, TEXT("Http telemetry ingestion attempt %s."), successful ? TEXT("succeeded") : TEXT("failed"));
            if (resp.IsValid())
            {
                UE_LOG(LogTelemetry, Display, TEXT("Http Ingestion Response Status: %d"), resp->GetResponseCode());
                if (resp->GetResponseCode() >= 400)
                {
                    UE_LOG(LogTelemetry, Error, TEXT("Telemetry Error Information: %s"), *resp->GetContentAsString());
                }
            }
            else
            {
                UE_LOG(LogTelemetry, Display, TEXT("No response from ingestion server."));
            }
        });

        request->ProcessRequest();
#endif
    }

private:
    FString IngestUrl;
};


/**
 * @brief For hooking in Simplygon API call for sending telemetry messages.
*/
class FSimplygonApiTelemetryWorker : public FTelemetryWorker
{
private:
	ISimplygonModule* SimplygonModule;
public:

	FSimplygonApiTelemetryWorker(ISimplygonModule* InModule, double SendInterval, int PendingBufferSize = 127, bool Compress = false) :
		FTelemetryWorker(SendInterval, PendingBufferSize, Compress)
	{
		SimplygonModule = InModule;
	}

	virtual ~FSimplygonApiTelemetryWorker() {}
	

	virtual void SendTelemetry(const FTelemetryProperties& CommonProperties)
	{
#ifdef ENABLED_TELEMETRY
		FTelemetryBatchPayload BatchPayload(CommonProperties);
		TSharedPtr<FTelemetryBuilder> Event;
		while (Pending.Dequeue(Event))
		{
			BatchPayload.AddTelemetry(Event->GetProperties());
		}

		const FString& Payload = BatchPayload.Finalize();

		
		UE_LOG(LogTelemetry, Display, TEXT("SimplygonTelemetryPayload: %d."), Payload.GetAllocatedSize());

		SimplygonModule->SendTelemetryData(Payload);

#endif
		
	}
};


/**
 * @brief A telemetry worker for logging json message to output window using UE_LOG Macro
*/
class FLoggerTelemetryWorker : public FTelemetryWorker
{
public:

	FLoggerTelemetryWorker(double SendInterval, int PendingBufferSize = 127, bool Compress = false) :
		FTelemetryWorker(SendInterval, PendingBufferSize, Compress)
	{
	}

	virtual ~FLoggerTelemetryWorker() {}

	virtual void SendTelemetry(const FTelemetryProperties& CommonProperties)
	{
#ifdef ENABLED_TELEMETRY
		FTelemetryBatchPayload BatchPayload(CommonProperties);
		TSharedPtr<FTelemetryBuilder> Event;
		while (Pending.Dequeue(Event))
		{
			BatchPayload.AddTelemetry(Event->GetProperties());
		}

		const FString& Payload = BatchPayload.Finalize();

		UE_LOG(LogTelemetry, Display, TEXT("SimplygonPluginTelemetryPayload: %s."), *Payload);
#endif
	}
};

static TUniquePtr<FTelemetryWorker> TelemetryWorker;

// Helper function to read configuration values from the ini
FString FTelemetryConfiguration::IniFileName = FString::Printf(TEXT("%sGameTelemetry.ini"), *FPaths::SourceConfigDir());
FString FTelemetryConfiguration::IniSectionName = TEXT("GameTelemetry");

FTelemetryConfiguration FTelemetryManager::GetConfigFromIni()
{
    FTelemetryConfiguration Config;
    FTelemetryConfiguration::GetString(TEXT("IngestUrl"), Config.IngestionUrl);
    FTelemetryConfiguration::GetDouble(TEXT("SendInterval"), Config.SendInterval);
    FTelemetryConfiguration::GetInt(TEXT("MaxBufferSize"), Config.PendingBufferSize);

    return Config;
}


FString FTelemetryManager::GetUniqueId()
{
	PRAGMA_DISABLE_DEPRECATION_WARNINGS
	static FGuid MachineId;
	FString MachineIdString;

	// Check to see if we already have a valid machine ID to use
	if (!MachineId.IsValid() && (!FPlatformMisc::GetStoredValue(TEXT("Microsoft"), TEXT("Simplygon/Identifiers"), TEXT("UnqiueId"), MachineIdString) || !FGuid::Parse(MachineIdString, MachineId)))
	{
		// No valid machine ID, found generate and save
		MachineId = FGuid::NewGuid();
		MachineIdString = MachineId.ToString(EGuidFormats::Digits);

		if (!FPlatformMisc::SetStoredValue(TEXT("Microsoft"), TEXT("Simplygon/Identifiers"), TEXT("UnqiueId"), MachineIdString))
		{
			// Failed to persist rreset to zero;
			MachineId = FGuid();
		}
	}
	
	// force an empty string if we cannot determine an ID.
	if (MachineId == FGuid())
	{
		return FString();
	}
	return MachineId.ToString(EGuidFormats::Digits).ToLower();
}


void FTelemetryManager::SetupCommonProperties()
{
#ifdef ENABLED_TELEMETRY
	//Build type
	Instance->CommonProperties.SetProperty(L"ue_build_type", LexToString(FApp::GetBuildConfiguration()));

	//Platform
	Instance->CommonProperties.SetProperty(L"ue_platform", FString(FPlatformProperties::IniPlatformName()));

	//Build Version
	Instance->CommonProperties.SetProperty(L"ue_build_version", FApp::GetBuildVersion());

	//is unattended
	Instance->CommonProperties.SetProperty(L"ue_is_unattended", FApp::IsUnattended());

#endif // ENABLED_TELEMETRY

	

	TSharedPtr<IPlugin> SimplygonPlugin = IPluginManager::Get().FindPlugin("Simplygon");
	FString PluginVersion = TEXT("None");
	if (SimplygonPlugin.IsValid())
	{
		const auto PluginDescriptor = SimplygonPlugin->GetDescriptor();
		PluginVersion = PluginDescriptor.VersionName;
	}

	//Plugin Version
	Instance->CommonProperties.SetProperty(L"ue_plugin_version", PluginVersion);
	
	
	
}

void FTelemetryManager::Initialize(const FTelemetryConfiguration & Config)
{
    if (hasInit)
    {
        Instance->Shutdown();
    }

	Instance->SetupCommonProperties();
    TelemetryWorker.Reset( new FHttpTelemetryWorker(Config.IngestionUrl, Config.SendInterval, Config.PendingBufferSize));
    hasInit = true;
}

void FTelemetryManager::Initialize()
{
	UE_LOG(LogTelemetry, Display, TEXT("Initialize Telemetry"));
	if (hasInit)
	{
		Instance->Shutdown();
	}

	Instance.Reset( new FTelemetryManager(FTelemetryManager()));

	Instance->SetupCommonProperties();
	auto Interval = 180;
	auto Settings = GetMutableDefault<USimplygonSettings>();
	auto bSendUsageData = true;
	if (Settings->IsValidLowLevelFast())
	{
		Interval = Settings->SendInterval;
	}
		
	if (!bSendUsageData)
		return;

	ISimplygonModule& Module = FModuleManager::LoadModuleChecked<ISimplygonModule>("Simplygon");
#if DEBUG_TELEMETRY
	TelemetryWorker = MakeUnique<FLoggerTelemetryWorker>(Interval);
#else
	TelemetryWorker.Reset(new FSimplygonApiTelemetryWorker(&Module, Interval));
#endif
	hasInit = true;

	UE_LOG(LogTelemetry, Display, TEXT("Telemetry Initialized"));
	
	
}

void FTelemetryManager::Record(const FString &Name, const FString &Category, FTelemetryBuilder &&Properties)
{
	UE_LOG(LogTelemetry, Display, TEXT("Record Telemetry"));
    if (hasInit)
    {
        TSharedPtr<FTelemetryBuilder> Evt(new FTelemetryBuilder(MoveTemp(Properties)));

        Evt->SetProperty(FTelemetry::ClientTimestamp());
        Evt->SetProperty(FTelemetry::EventName(Name));
        Evt->SetProperty(FTelemetry::Category(Category));


        uint32 CurrentSequence = Sequence++;
        Evt->SetProperty(TEXT("ue_event_seq"), FVariant(CurrentSequence));

        TelemetryWorker->Enqueue(Evt);
    }
    else
    {
        UE_LOG(LogTelemetry, Error, TEXT("Cannot record event because the telemetry subsystem has not been initialized."));
    }

}

inline void FTelemetryManager::SetClientId(const FString & InClientId)
{
    Instance->CommonProperties.SetProperty(FTelemetry::ClientId(InClientId));
}

inline void FTelemetryManager::SetSessionId(const FString & InSessionId)
{
    Instance->CommonProperties.SetProperty(FTelemetry::SessionId(InSessionId));
}


void FTelemetryManager::Shutdown()
{
	UE_LOG(LogTelemetry, Display, TEXT("Shutdown Telemetry Worker."));
    if (hasInit)
    {
        TelemetryWorker->Stop();
		TelemetryWorker.Reset();
    }
}
